/**
 * Copyright 2009-2020 the original author or authors.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.ibatis.mapping;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 底层封装对象
 * 作用：对操作数据库存储封装，包括 sql 语句、输入输出参数
 *
 * @author Clinton Begin
 */
public final class MappedStatement {

    /** Mapper配置文件路径 */
    private String resource;
    /** Configuration对象的引用，方便获取MyBatis配置信息以及TypeHandler、TypeAlias等信息 */
    private Configuration configuration;
    /** 命名空间唯一标识 */
    private String id;
    /** 设置JDBC中Statement对象的fetchSize属性，用于指定SQL执行后返回的最大行数 */
    private Integer fetchSize;
    /** 驱动程序等待数据库返回请求结果的秒数，超时抛出异常 */
    private Integer timeout;
    /**
     * 参数可选：STATEMENT、PREPARED、CALLABLE
     * 这样分别使用Statement、PreparedStatement、CallableStatement与数据库交互，默认为PREPARED
     */
    private StatementType statementType;
    /**
     * 参数可选：FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE
     * 用来设置ResultSet对象的特征，如果没设置，则由JDBC驱动决定
     */
    private ResultSetType resultSetType;
    /**
     * 解析<select|update|insert|delete>
     * 将SQL语句配置信息解析为SqlSource对象
     */
    private SqlSource sqlSource;
    /**
     * 二级缓存实例
     * 根据Mapper中的<cache>标签配置信息创建对应的Cache实现
     */
    private Cache cache;
    /**
     * 用于引用通过<parameterMap>标签定义的参数映射，已废弃
     */
    private ParameterMap parameterMap;
    /**
     * 用于引用通过<resultMap>标签配置的实体属性与数据库字段之间的简历的结果集映射（注意和resultType不能混用）
     */
    private List<ResultMap> resultMaps;
    /**
     * 用于是否强制刷新缓存
     * true的时候，任何时候语句被调用都会导致本地缓存以及二级缓存清空，默认false
     */
    private boolean flushCacheRequired;
    /**
     * 是否使用二级缓存
     * 默认true
     */
    private boolean useCache;
    /**
     * 针对嵌套结果select语句
     * true：假定嵌套结果包含在一起或者分组在一起，这样的话，当返回一个主结果行的时候，就不会发生对前面结果集引用的情况。
     * 这样的话，在获取嵌套结果集的时候不至于内存不够用，默认为false
     */
    private boolean resultOrdered;
    /**
     * 一个枚举类
     * 用来指定SQL操作类型（增删改查还是未知等）
     */
    private SqlCommandType sqlCommandType;
    /**
     * 主键生成策略，默认是Jdbc3KeyGenerator，也就是数据库主键自增
     * 配置了<selectKey>的话，则使用SelectKeyGenerator生成主键
     */
    private KeyGenerator keyGenerator;
    /**
     * 针对<update|insert>
     * 用于将数据库自增主键或者<insert>标签中的<selectKey>标签返回的值填充到实体的属性中，如果有多个属性，使用逗号分隔
     */
    private String[] keyProperties;
    /**
     * 针对<update|insert>
     * 通过生成的键值设置表的列名，（在某些数据库【PostgreSQL】中是必须的），当主键列不是表中的第一列的时候需要设置，如果有多个字段，使用逗号分隔
     */
    private String[] keyColumns;
    /**
     * <select>标签中通过resultMap属性指定ResultMap是不是嵌套的ResultMap
     */
    private boolean hasNestedResultMaps;
    /**
     * 如果配置了databaseIdProvider，MyBatis会加载
     */
    private String databaseId;
    /** 用来输出日志 */
    private Log statementLog;
    /**
     * 该属性用于指定LanguageDriver实现，MyBatis中的LanguageDriver用于解析<select|update|insert|delete>标签中的SQL语句，生成SqlSource对象
     */
    private LanguageDriver lang;
    /**
     * 仅针对多结果集
     * 它将列出语句执行后返回的结果集并给结果集一个名称，名称使用逗号分隔
     */
    private String[] resultSets;

    MappedStatement() {
        // constructor disabled
    }

    public static class Builder {
        private MappedStatement mappedStatement = new MappedStatement();

        public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
            mappedStatement.configuration = configuration;
            mappedStatement.id = id;
            mappedStatement.sqlSource = sqlSource;
            mappedStatement.statementType = StatementType.PREPARED;
            mappedStatement.resultSetType = ResultSetType.DEFAULT;
            mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<>()).build();
            mappedStatement.resultMaps = new ArrayList<>();
            mappedStatement.sqlCommandType = sqlCommandType;
            mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
            String logId = id;
            if (configuration.getLogPrefix() != null) {
                logId = configuration.getLogPrefix() + id;
            }
            mappedStatement.statementLog = LogFactory.getLog(logId);
            mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
        }

        public Builder resource(String resource) {
            mappedStatement.resource = resource;
            return this;
        }

        public String id() {
            return mappedStatement.id;
        }

        public Builder parameterMap(ParameterMap parameterMap) {
            mappedStatement.parameterMap = parameterMap;
            return this;
        }

        public Builder resultMaps(List<ResultMap> resultMaps) {
            mappedStatement.resultMaps = resultMaps;
            for (ResultMap resultMap : resultMaps) {
                mappedStatement.hasNestedResultMaps = mappedStatement.hasNestedResultMaps || resultMap.hasNestedResultMaps();
            }
            return this;
        }

        public Builder fetchSize(Integer fetchSize) {
            mappedStatement.fetchSize = fetchSize;
            return this;
        }

        public Builder timeout(Integer timeout) {
            mappedStatement.timeout = timeout;
            return this;
        }

        public Builder statementType(StatementType statementType) {
            mappedStatement.statementType = statementType;
            return this;
        }

        public Builder resultSetType(ResultSetType resultSetType) {
            mappedStatement.resultSetType = resultSetType == null ? ResultSetType.DEFAULT : resultSetType;
            return this;
        }

        public Builder cache(Cache cache) {
            mappedStatement.cache = cache;
            return this;
        }

        public Builder flushCacheRequired(boolean flushCacheRequired) {
            mappedStatement.flushCacheRequired = flushCacheRequired;
            return this;
        }

        public Builder useCache(boolean useCache) {
            mappedStatement.useCache = useCache;
            return this;
        }

        public Builder resultOrdered(boolean resultOrdered) {
            mappedStatement.resultOrdered = resultOrdered;
            return this;
        }

        public Builder keyGenerator(KeyGenerator keyGenerator) {
            mappedStatement.keyGenerator = keyGenerator;
            return this;
        }

        public Builder keyProperty(String keyProperty) {
            mappedStatement.keyProperties = delimitedStringToArray(keyProperty);
            return this;
        }

        public Builder keyColumn(String keyColumn) {
            mappedStatement.keyColumns = delimitedStringToArray(keyColumn);
            return this;
        }

        public Builder databaseId(String databaseId) {
            mappedStatement.databaseId = databaseId;
            return this;
        }

        public Builder lang(LanguageDriver driver) {
            mappedStatement.lang = driver;
            return this;
        }

        public Builder resultSets(String resultSet) {
            mappedStatement.resultSets = delimitedStringToArray(resultSet);
            return this;
        }

        /**
         * Resul sets.
         *
         * @param resultSet the result set
         *
         * @return the builder
         *
         * @deprecated Use {@link #resultSets}
         */
        @Deprecated
        public Builder resulSets(String resultSet) {
            mappedStatement.resultSets = delimitedStringToArray(resultSet);
            return this;
        }

        public MappedStatement build() {
            assert mappedStatement.configuration != null;
            assert mappedStatement.id != null;
            assert mappedStatement.sqlSource != null;
            assert mappedStatement.lang != null;
            mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);
            return mappedStatement;
        }
    }

    public KeyGenerator getKeyGenerator() {
        return keyGenerator;
    }

    public SqlCommandType getSqlCommandType() {
        return sqlCommandType;
    }

    public String getResource() {
        return resource;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public String getId() {
        return id;
    }

    public boolean hasNestedResultMaps() {
        return hasNestedResultMaps;
    }

    public Integer getFetchSize() {
        return fetchSize;
    }

    public Integer getTimeout() {
        return timeout;
    }

    public StatementType getStatementType() {
        return statementType;
    }

    public ResultSetType getResultSetType() {
        return resultSetType;
    }

    public SqlSource getSqlSource() {
        return sqlSource;
    }

    public ParameterMap getParameterMap() {
        return parameterMap;
    }

    public List<ResultMap> getResultMaps() {
        return resultMaps;
    }

    public Cache getCache() {
        return cache;
    }

    public boolean isFlushCacheRequired() {
        return flushCacheRequired;
    }

    public boolean isUseCache() {
        return useCache;
    }

    public boolean isResultOrdered() {
        return resultOrdered;
    }

    public String getDatabaseId() {
        return databaseId;
    }

    public String[] getKeyProperties() {
        return keyProperties;
    }

    public String[] getKeyColumns() {
        return keyColumns;
    }

    public Log getStatementLog() {
        return statementLog;
    }

    public LanguageDriver getLang() {
        return lang;
    }

    public String[] getResultSets() {
        return resultSets;
    }

    /**
     * Gets the resul sets.
     *
     * @return the resul sets
     *
     * @deprecated Use {@link #getResultSets()}
     */
    @Deprecated
    public String[] getResulSets() {
        return resultSets;
    }

    public BoundSql getBoundSql(Object parameterObject) {
        // 调用SqlSource对象的getBoundSql方法解析称为BoundSql对象
        BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings == null || parameterMappings.isEmpty()) {
            boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
        }

        // check for nested result maps in parameter mappings (issue #30)
        for (ParameterMapping pm : boundSql.getParameterMappings()) {
            String rmId = pm.getResultMapId();
            if (rmId != null) {
                ResultMap rm = configuration.getResultMap(rmId);
                if (rm != null) {
                    hasNestedResultMaps |= rm.hasNestedResultMaps();
                }
            }
        }

        return boundSql;
    }

    private static String[] delimitedStringToArray(String in) {
        if (in == null || in.trim().length() == 0) {
            return null;
        } else {
            return in.split(",");
        }
    }

}
